import cPickle as pickle
import gobject
import time
import socket
import asyncore
import thread
import copy

import re
import sys
import os


from net import network, queues
from util import config, device
from db import db

# TODO: 
# - Resilience to network failure
# - Stubbing framework?
# - Move UserPresence (etc) to Presence.User and fix refs?
# Status documentation (fv vs. presence status)
OFFLINE = None
ONLINE = None

import logging
logger = logging.getLogger('presence')

class Presence(gobject.GObject):

    __gsignals__ = {
        'offline' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_STRING,))
    }
   
    def __init__(self, pid, name):
        gobject.GObject.__init__(self)
        self.pid = pid
        self.name = name
        self.updated = time.time()
        self.type = self.__class__.__name__
       
    def __eq__(self, other):
        return (self.pid == other.pid and self.__class__ == other.__class__)
       
    def __hash__(self):
        return hash(self.pid)
       
    def __repr__(self): 
        return '<Presence: %s,%s>' % (self.pid, self.name)

class UserPresence(Presence):

    def __init__(self, uid, name, ip, meta = {}):
        Presence.__init__(self, uid, name)
        self.ip = ip
        self.meta = meta
        self.objects = []
        self.distance = config.get_int('presence', 'max-distance')
        self.status = ONLINE
        
    def watch(self, offline_func):
        gobject.GObject.__init__(self)
        self.__check_online_timer = gobject.timeout_add(config.get_int('presence', 'offline-check-timeout-ms'), self.__check_online)
        self.__update_distance_timer = gobject.timeout_add(config.get_int('presence', 'distance-check-timeout-ms'), self.__update_distance)
        self.connect('offline', offline_func)
        self.updated = time.time()
        # Do the initial distance update.
        def initial_update():
            self.__update_distance()
            return False
        gobject.idle_add(initial_update)

    def has_at_all(self, pid):
        for obj in self.objects:
            if obj.pid == pid: return True
        return False

    def has_complete(self, pid):
        for obj in self.objects:
            if obj.pid == pid and not isinstance(obj, PartialPresence): return True
        return False

    def add_object(self, obj):
        assert obj not in self.objects
        self.objects.append(obj)
        assert obj in self.objects
   
    def remove_object(self, obj):
        assert obj in self.objects
        self.objects.remove(obj)
        assert obj not in self.objects

    def replace_with_new(self, new):
        old = None
        for o in self.objects:
            if o.pid == new.pid: old = o
        assert old is not None
        self.objects.remove(old)
        self.objects.append(new)
            
    def alive(self):
        self.updated = time.time()
    
    def __update_distance(self):
        logger.info('Updating distance for user %i', self.pid)
        if device.current() == device.NOKIA_TABLET:
            command = 'sudo ping -q -c3 ' + self.ip
            regex = re.compile("round-trip min/avg/max = (?P<min>[0-9]+\.[0-9]+)/(?P<avg>[0-9]+\.[0-9]+)/(?P<max>[0-9]+\.[0-9]+)")
        else:
            command = 'ping -q -c3 ' + self.ip
            regex = re.compile("rtt min/avg/max/mdev = (?P<min>[0-9]+\.[0-9]+)/(?P<avg>[0-9]+\.[0-9]+)/(?P<max>[0-9]+\.[0-9]+)/(?P<mdev>[0-9]+\.[0-9]+)")
        
        ping = os.popen(command, 'r')
        sys.stdout.flush()
        updated = False
        while True:
            line = ping.readline()
            if not line: break
            match = regex.match(line)
            if match:
                avg = float(match.group('avg'))
                updated = True
   
        if updated:
            self.distance = avg
        else:
            self.distance = config.get_int('presence', 'max-distance')
        logger.info('Updated distance for user %i. New distance: %s', self.pid, self.distance)
        return True

    def __check_online(self):
        offline_period = config.get_int('presence', 'offline-period-s')
        logger.info('Checking if user %i is offline. Last update: %s. Now: %s. Max offline period: %s', self.pid, self.updated, time.time(), offline_period)
        if time.time() - self.updated > offline_period:
            self.status = OFFLINE
            gobject.source_remove(self.__check_online_timer)
            gobject.source_remove(self.__update_distance_timer)
            self.emit('offline', self.pid)
            return False
        else:
            return True
       
class ServicePresence(UserPresence):

    def __init__(self, sid, name, ip, meta = {}):
        UserPresence.__init__(self, sid, name, ip, meta)


class ObjectPresence(Presence):

    def __init__(self, oid, name):
        Presence.__init__(self, oid, name)
       
class PartialPresence(ObjectPresence):

    def __init__(self, oid):
        ObjectPresence.__init__(self, oid, '<partial>')

class ExpiredPresence(ObjectPresence):

    def __init__(self, pid):
        ObjectPresence.__init__(self, pid, '<expired>')

class DeliveredPresence(ObjectPresence):
    
    def __init__(self, pid):
        ObjectPresence.__init__(self, pid, '<delivery>')

class PresenceService(gobject.GObject):
   
    __gsignals__ = {
        'neighbor-online' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_PYOBJECT,gobject.TYPE_BOOLEAN)),
        'neighbor-update' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_PYOBJECT,)),
        'neighbor-offline' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_PYOBJECT,)),
        'object-appeared' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_PYOBJECT,)),
        'object-disappeared' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
                (gobject.TYPE_PYOBJECT,)),
    }       
       
    def __init__(self, iface, my_id, my_name, standalone=True, config_files=[]):
        gobject.GObject.__init__(self)
        config.add_file(os.path.join(os.path.dirname(__file__), 'presence.config'))
        if config_files:
            config.add_files(config_files)
            
        global OFFLINE, ONLINE
        OFFLINE = config.get_int('presence.status', 'offline')
        ONLINE = config.get_int('presence.status', 'online')
        self.me = UserPresence(my_id, my_name, network.get_ip_addr(iface))
        # I'd much rather use a set() for present and offline, but because you
        # can't retrieve elements from a set, I'm forced to use dictionaries.
        self.__present = {}
        self.__offline = {}
        self.__queue = queues.Queue()
        self.__socket = network.Broadcast(iface, config.get_int('presence', 'port'), self.__queue, self.__receive)
        
        self.__PRESENCE_INTERVAL = config.get_int('presence', 'presence-interval-ms')
        self.__beat_timer = gobject.timeout_add(self.__PRESENCE_INTERVAL, self.__beat)

        self.__table = db.get_database(config.get('presence', 'db-dir'), config.get('presence', 'db-file')).get_or_create(PresenceTable)
        
        if standalone:
            network.start()

    def add_meta(self, key, value):
        if key in self.me.meta:
            del self.me.meta[key]
        self.me.meta[key] = value
       
    def add_object(self, obj):
        self.me.add_object(obj)
       
    def remove_object(self, obj):
        self.me.remove_object(obj)

    def replace_with_new(self, obj):
        self.me.replace_with_new(obj)

    def get_known_user(self, uid):
        user = self.__table.get_historical(uid)
        self.__add_fields_to_user(user)
        return user
    
    def get_all_known_users(self):
        users = self.__table.get_all_historical()
        for user in users:
            self.__add_fields_to_user(user)
        return users
    
    # This is a total BS method for adding some metadata to users that
    # can be accessed outside of the class.
    def __add_fields_to_user(self, user):
        present = self.__present.get(user['uid'], None)
        if not present:
            user['status'] = OFFLINE
        else:
            user['status'] = ONLINE
            user['meta'] = present.meta
            user['type'] = present.type

    def get_neighbor(self, nid):
        return self.__present[nid]
       
    def get_neighbors(self):
        return copy.copy(self.__present.values())
       
    def who_has_complete(self, pid):
        neighbors = []
        for neighbor in self.__present.values():
            if neighbor.has_complete(pid): neighbors.append(neighbor)
        return neighbors

    def is_unique(self, obj):
        return len(self.who_has_complete(obj.pid)) == 0 and not self.me.has_complete(obj.pid)
   
    def __calc_diffs(self, old, new):
        added = []
        removed = []
        for item in new:
            if item not in old:
                added.append(item)
        for item in old:
            if item not in new:
                removed.append(item)
        return added, removed

    def __receive(self, data, addr):
        neighbor = pickle.loads(data)
        logger.debug('Received presence. Neighbor %i from addr %s', neighbor.pid, addr)
        # Quick sanity check. :)
        neighbor.is_me = False
        # Save the user and let us know if the neighbor is new.
        new = self.__table.save_user(neighbor)
        if neighbor.pid in self.__present:
            old = self.__present[neighbor.pid]
            old.alive()
            if neighbor.meta != old.meta or neighbor.name != old.name:
                old.meta = neighbor.meta
                old.name = neighbor.name
                logger.info('Neighbor %i was updated with new meta %s', old.pid, old.meta)
                self.emit('neighbor-update', old)
            if old.objects != neighbor.objects:
                added, removed = self.__calc_diffs(old.objects, neighbor.objects)
                # This is a hack to make is_unique work.
                old.objects = []
                for obj in added:
                    if self.is_unique(obj):
                        logger.info('New object %i appeared at addr %s', obj.pid, addr)
                        self.emit('object-appeared', obj)
                for obj in removed:
                    if self.is_unique(obj):
                        logger.info('Last object %i disappeared from addr %s', obj.pid, addr)
                        self.emit('object-disappeared', obj)
                old.objects = neighbor.objects
        else:
            if neighbor.pid in self.__offline:
                del self.__offline[neighbor.pid]
            neighbor.watch(self.__on_offline)            
            for obj in neighbor.objects:
                if self.is_unique(obj):
                    logger.info('New object %i appeared at addr %s', obj.pid, addr) 
                    self.emit('object-appeared', obj) 
            self.__present[neighbor.pid] = neighbor
            logger.info('New neighbor %i appeared at addr %s', neighbor.pid, addr)
            self.emit('neighbor-online', neighbor, new)

        self.__recalc_presence_interval()
       
    def __recalc_presence_interval(self):
        changed = False
       
        count = len(self.__present)
        if count > 10:
            if self.__PRESENCE_INTERVAL != 20000:
                self.__PRESENCE_INTERVAL = 20000
                changed = True
        elif count > 5:
            if self.__PRESENCE_INTERVAL != 10000:
                self.__PRESENCE_INTERVAL = 10000
                changed = True
        else:
            if self.__PRESENCE_INTERVAL != 5000:
                self.__PRESENCE_INTERVAL = 5000
                changed = True
               
        if changed:
            logger.info('Changing presence interval to %i, because %i users are online', self.__PRESENCE_INTERVAL, count)
            gobject.source_remove(self.__beat_timer)
            self.__beat_timer = gobject.timeout_add(self.__PRESENCE_INTERVAL, self.__beat)
   
    def __beat(self):
        logger.debug('Sending heartbeat')
        pickled = pickle.dumps(self.me, pickle.HIGHEST_PROTOCOL)
        self.__queue.put(pickled)
        return True

    def __on_offline(self, neighbor, pid):
        logger.info('Neighbor %i went offline',neighbor.pid)
        self.__offline[neighbor.pid] = neighbor
        del self.__present[neighbor.pid]
        
        for obj in neighbor.objects:
            if self.is_unique(obj): self.emit('object-disappeared', obj)
        self.emit('neighbor-offline', neighbor)
       
    # Testing hook
    def run(self):
        self.loop = gobject.MainLoop()
        self.loop.run()
       
gobject.type_register(Presence)
gobject.type_register(PresenceService)


class PresenceTable(db.Table):
    def __init__(self):
        db.Table.__init__(self)
        
    def _init(self):
        self._conn.execute('''
            CREATE TABLE presence
            (uid INTEGER PRIMARY KEY NOT NULL, name STRING NOT NULL, timestamp INTEGER NOT NULL)
            ''')
       
    def _drop(self):
        self._conn.execute('''
            DROP TABLE presence
            ''')
        
    def save_user(self, user):
        new = False
        c = self._conn
        [result] = c.execute('''
            SELECT COUNT(*) as count FROM presence WHERE uid=?
            ''', (user.pid,))
        count = result['count']
        if count > 0:
            c.execute('''
            UPDATE presence
            SET name=?, timestamp=?
            WHERE uid=?''', (user.name, time.time(), user.pid))
        else:
            new = True
            c.execute('''
            INSERT INTO presence VALUES (?,?,?)''', (user.pid, user.name, time.time()))
        return new
    
    def get_historical_info(self, uid):
        c = self._conn
        [result] = c.execute('''
            SELECT * FROM presence WHERE uid=?
            ''', (uid,))
        return result
    
    def get_all_historical(self):
        c = self._conn
        return c.execute(''' SELECT * FROM presence ''')
    
    
# The following is all test code    
if __name__ == "__main__":
    import sys
    import fcntl
    import struct
    import random

    def GetWirelessInterfaceName():
        hostname = socket.gethostname()
        if ("N800" or "n800" or "N810" or "n810") in hostname:
            return "wlan0"
        elif "viraLBaby" in hostname:
            return "eth1" 
        return "wlan0"  
#        return "eth0"
    
    def GetIPAddress(ifname):
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        return socket.inet_ntoa(fcntl.ioctl(
                s.fileno(),
                0x8915, # SIOCGIFADDR
                struct.pack('256s', ifname[:15])
        )[20:24])

    def GetBcastAddress(ifname):
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        return socket.inet_ntoa(fcntl.ioctl(
            s.fileno(), 
            0x8919, # SIOCGIFBRDADDR
            struct.pack('256s', ifname[:15])
        )[20:24])
   
    def Online(obj, presence):
        print time.time(),presence.name,"came online with objects",presence.objects

    def Offline(obj, presence):
        print time.time(),presence.name,"went offline."

    def Update(obj, presence):
        print time.time(),presence.name,"was updated."

    def ObjectAppeared(obj, presence):
        print time.time(),"Object '",presence.name,"' appeared."

    def ObjectDisappeared(obj, presence):
        print time.time(),"Object '",presence.name,"' disappeared."

    if len(sys.argv) < 5:
        print "usage: python presence.py <send_port> <recv_port> <my_id> <my_name>"
        exit(0)

    iface = GetWirelessInterfaceName()
    print iface
    my_ip = GetIPAddress(iface)
    broadcast_ip = GetBcastAddress(iface)
    print my_ip, broadcast_ip
       
    send_port = int(sys.argv[1])
    recv_port = int(sys.argv[2])
    my_id = sys.argv[3]
    my_name = sys.argv[4]
    #ip, port, my_name
    pres = PresenceService(my_ip, broadcast_ip, send_port, recv_port, my_id, my_name)

    pres.add_object(AudioMsgPresence(random.randint(1,100) , "name" + str(random.randint(1,100)), my_ip))
    # connect to events of presence
    pres.connect('neighbor-online', Online)
    pres.connect('neighbor-offline', Offline)
    pres.connect('neighbor-update', Update)
    pres.connect('object-appeared', ObjectAppeared)
    pres.connect('object-disappeared', ObjectDisappeared)

    gobject.threads_init()
    loop = gobject.MainLoop()
    loop.run()

"""
ping_string = {
    'ubuntu'   : ,
    'maemo'  : ',
}
"""
